package net.tootallnate.websocket;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.UnresolvedAddressException;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import net.tootallnate.websocket.drafts.Draft_10;
import net.tootallnate.websocket.exeptions.InvalidHandshakeException;
/**
* The <tt>WebSocketClient</tt> is an abstract class that expects a valid
* "ws://" URI to connect to. When connected, an instance recieves important
* events related to the life of the connection. A subclass must implement
* <var>onOpen</var>, <var>onClose</var>, and <var>onMessage</var> to be
* useful. An instance can send messages to it's connected server via the
* <var>send</var> method.
*
* @author Nathan Rajlich
*/
public abstract class WebSocketClient extends WebSocketAdapter implements Runnable {
// INSTANCE PROPERTIES /////////////////////////////////////////////////////
/**
* The URI this client is supposed to connect to.
*/
private URI uri = null;
/**
* The WebSocket instance this client object wraps.
*/
private WebSocket conn = null;
/**
* The SocketChannel instance this client uses.
*/
private SocketChannel client = null;
/**
* The 'Selector' used to get event keys from the underlying socket.
*/
private Selector selector = null;
private Thread thread;
private Draft draft;
final Lock closelock = new ReentrantLock();
// CONSTRUCTORS ////////////////////////////////////////////////////////////
public WebSocketClient( URI serverURI ) {
this( serverURI, new Draft_10() );
}
/**
* Constructs a WebSocketClient instance and sets it to the connect to the
* specified URI. The client does not attampt to connect automatically. You
* must call <var>connect</var> first to initiate the socket connection.
*/
public WebSocketClient( URI serverUri , Draft draft ) {
if( serverUri == null ) {
throw new IllegalArgumentException();
}
if( draft == null ) {
throw new IllegalArgumentException( "null as draft is permitted for `WebSocketServer` only!" );
}
this.uri = serverUri;
this.draft = draft;
}
// PUBLIC INSTANCE METHODS /////////////////////////////////////////////////
/**
* Gets the URI that this WebSocketClient is connected to.
*
* @return The <tt>URI</tt> for this WebSocketClient.
*/
public URI getURI() {
return uri;
}
public Draft getDraft() {
return draft;
}
/**
* Starts a background thread that attempts and maintains a WebSocket
* connection to the URI specified in the constructor or via <var>setURI</var>.
* <var>setURI</var>.
*/
public void connect() {
if( thread != null )
throw new IllegalStateException( "already/still connected" );
thread = new Thread( this );
thread.start();
}
public void close() {
if( thread != null ) {
thread.interrupt();
closelock.lock();
if( selector != null )
selector.wakeup();
closelock.unlock();
}
}
/**
* Sends <var>text</var> to the connected WebSocket server.
*
* @param text
* The String to send to the WebSocket server.
*/
public void send( String text ) throws NotYetConnectedException , InterruptedException {
if( conn != null ) {
conn.send( text );
}
}
private void tryToConnect( InetSocketAddress remote ) throws IOException {
client = SocketChannel.open();
client.configureBlocking( false );
client.connect( remote );
selector = Selector.open();
client.register( selector, SelectionKey.OP_CONNECT );
}
// Runnable IMPLEMENTATION /////////////////////////////////////////////////
public void run() {
if( thread == null )
thread = Thread.currentThread();
interruptableRun();
thread = null;
}
protected void interruptableRun() {
try {
tryToConnect( new InetSocketAddress( uri.getHost(), getPort() ) );
} catch ( ClosedByInterruptException e ) {
onError( null, e );
return;
} catch ( IOException e ) {//
onError( conn, e );
return;
} catch ( SecurityException e ) {
onError( conn, e );
return;
} catch ( UnresolvedAddressException e ) {
onError( conn, e );
return;
}
conn = new WebSocket( this, draft, client );
try/*IO*/{
while ( !conn.isClosed() ) {
if( Thread.interrupted() ) {
conn.close( CloseFrame.NORMAL );
}
SelectionKey key = null;
conn.flush();
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> i = keys.iterator();
while ( i.hasNext() ) {
key = i.next();
i.remove();
if( key.isReadable() ) {
conn.handleRead();
}
if( !key.isValid() ) {
continue;
}
if( key.isWritable() ) {
conn.flush();
}
if( key.isConnectable() ) {
try {
finishConnect();
} catch ( InterruptedException e ) {
conn.close( CloseFrame.NEVERCONNECTED );// report error to only
break;
} catch ( InvalidHandshakeException e ) {
conn.close( e ); // http error
conn.flush();
}
}
}
}
} catch ( IOException e ) {
onError( e );
conn.close( CloseFrame.ABNROMAL_CLOSE );
return;
} catch ( RuntimeException e ) {
// this catch case covers internal errors only and indicates a bug in this websocket implementation
onError( e );
conn.closeConnection( CloseFrame.BUGGYCLOSE, e.toString(), false );
return;
}
try {
selector.close();
} catch ( IOException e ) {
onError( e );
}
closelock.lock();
selector = null;
closelock.unlock();
try {
client.close();
} catch ( IOException e ) {
onError( e );
}
client = null;
}
private int getPort() {
int port = uri.getPort();
return port == -1 ? WebSocket.DEFAULT_PORT : port;
}
private void finishConnect() throws IOException , InvalidHandshakeException , InterruptedException {
if( client.isConnectionPending() ) {
client.finishConnect();
}
// Now that we're connected, re-register for only 'READ' keys.
client.register( selector, SelectionKey.OP_READ );
sendHandshake();
}
private void sendHandshake() throws IOException , InvalidHandshakeException , InterruptedException {
String path;
String part1 = uri.getPath();
String part2 = uri.getQuery();
if( part1 == null || part1.length() == 0 )
path = "/";
else
path = part1;
if( part2 != null )
path += "?" + part2;
int port = getPort();
String host = uri.getHost() + ( port != WebSocket.DEFAULT_PORT ? ":" + port : "" );
HandshakedataImpl1 handshake = new HandshakedataImpl1();
handshake.setResourceDescriptor( path );
handshake.put( "Host", host );
conn.startHandshake( handshake );
}
/**
* Calls subclass' implementation of <var>onMessage</var>.
*
* @param conn
* @param message
*/
@Override
public void onMessage( WebSocket conn, String message ) {
onMessage( message );
}
/**
* Calls subclass' implementation of <var>onOpen</var>.
*
* @param conn
*/
@Override
public void onOpen( WebSocket conn, Handshakedata handshake ) {
onOpen( handshake );
}
/**
* Calls subclass' implementation of <var>onClose</var>.
*
* @param conn
*/
@Override
public void onClose( WebSocket conn, int code, String reason, boolean remote ) {
thread.interrupt();
onClose( code, reason, remote );
}
/**
* Calls subclass' implementation of <var>onIOError</var>.
*
* @param conn
*/
@Override
public void onError( WebSocket conn, Exception ex ) {
onError( ex );
}
@Override
public void onWriteDemand( WebSocket conn ) {
selector.wakeup();
}
// ABTRACT METHODS /////////////////////////////////////////////////////////
public abstract void onMessage( String message );
public abstract void onOpen( Handshakedata handshakedata );
public abstract void onClose( int code, String reason, boolean remote );
public abstract void onError( Exception ex );
}